from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import render, get_object_or_404
from django.urls import reverse
from django.utils import timezone
from django.views.generic.list import ListView

from .models import Player, Transaction, Stock
from .forms import NewPlayerForm
from .decorators import check_player_and_lost

from .backend import stock_controller as StockPriceScraper

@login_required
@check_player_and_lost
def main(request):
    player = Player.objects.get(pk=request.user)
    
    context = {
        "player": player,
    }
    # send main game response
    return HttpResponse(render(request, "zylinder_app/main.html", context))

###PLAYER VIEWS#

@login_required
def newPlayer(request):
    if request.method == "POST":
        # check if player already exists
        try:
            Player.objects.get(pk=request.user)
            # if this didnt throw an exception redirect to the game
            return HttpResponseRedirect(reverse("zylinder_app:main"))
        except Player.DoesNotExist:
            form = NewPlayerForm(request.POST)
            if form.is_valid():
                data = form.save(commit=False)
                data.djangouser = request.user
                data.capital = data.startcapital
                data.save()
                return HttpResponseRedirect(reverse("zylinder_app:main"))
    else:
        form = NewPlayerForm()
    return render(request, "zylinder_app/newPlayer.html", {"form": form})

@login_required
def hasLost(request):
    """return True if lost, else False"""
    try:
        player = Player.objects.get(pk=request.user)
    except Player.DoesNotExist:
        return HttpResponseRedirect(reverse("zylinder_app:newPlayer"))
    return HttpResponse(player.lost, content_type="text/plain")

@login_required
def youLost(request):
    try:
        player = Player.objects.get(pk=request.user)
    except Player.DoesNotExist:
        return HttpResponseRedirect(reverse("zylinder_app:newPlayer"))
    # check if really lost
    if not player.lost:
        return HttpResponseRedirect(reverse("zylinder_app:main"))

    context = {
        "startcapital":         player.startcapital,
        "acceptedloss":         player.acceptedloss,
        "acceptedlossabsolute": round(player.lostcapitaltimesten/10, 2)
    }
    return HttpResponse(render(request, "zylinder_app/youLost.html", context))

@login_required
def resetPlayer(request):
    """View which resets the player. No JS-confirm() because of CSRF"""
    try:
        player = Player.objects.get(pk=request.user)
    except Player.DoesNotExist:
        return HttpResponseRedirect(reverse("zylinder_app:newPlayer"))

    if request.method == "POST":
        # reset the player
        player.delete()
        return HttpResponseRedirect(reverse("zylinder_app:main"))
    else:
        return render(request, "zylinder_app/resetPlayer.html", {"player": player})

class TradeHistoryView(LoginRequiredMixin, ListView):
    model = Transaction
    template_name = "zylinder_app/tradeHistory.html"
    def get_queryset(self):
        try:
            player = Player.objects.get(pk=self.request.user)
        except Player.DoesNotExist:
            return HttpResponseRedirect(reverse("zylinder_app:newPlayer"))
        trans = Transaction.objects.filter(player=player)
        return trans.order_by("buydate")

@login_required
@check_player_and_lost
def getMoney(request):
    """return the player money plain in the body.

    return: plain int in http body
    """
    player = Player.objects.get(pk=request.user)
    return HttpResponse(player.capital, content_type="text/plain")

@login_required
@check_player_and_lost
def getCurrentStocks(request):
    """return a json of current stocks under the key "stocks" sorted by buydate

    return: json with stocks
    """
    player = Player.objects.get(pk=request.user)
    stocks = Transaction.objects.filter(player=player, selldate=None).order_by("buydate")
    resp = []
    for stock in stocks:
        resp.append({
            "symbol": stock.symbol.symbol,
            "buydate": stock.buydate,
            "boughtfor": stock.boughtfor
        })
    return JsonResponse(resp, safe=False)


### STOCK VIEWS ###
def getAvailableStocks(request):
    allStocks = list(Stock.objects.values_list('symbol', flat=True))
    return JsonResponse(allStocks, safe=False)

getStocksRunning = False
@login_required
@check_player_and_lost
def getStocks(request):
    """gets stocks of the player of the request, including amount of value of a single stocck
    also sets if the user lost and player attributes lostcapitaltimesten with lost
    NOTE: this is currently in single user mode for the exam - meaning
          only one request at a time. for multi user, there has to be a 
          complex caching and anti-abuse and regulatory system, which is 
          out of scope here. 
          The limitation for one user makes it easier to avoid race conditions.
    return:
        json {"SYML:" {"amount": 1, "value": 100, "total_value": 100}, "_all": 100}
    """

    global getStocksRunning
    if not getStocksRunning:
      getStocksRunning = True
      player = Player.objects.get(pk=request.user)
      stocks = Transaction.objects.filter(player=player, selldate=None)

      try:
          values = __getStockValuesFor(stocks)
          hasLost = __hasLost(player, values)
          print(hasLost)
          if hasLost:
              player.lost = True
              player.lostcapitaltimesten = hasLost
              player.save()
              # return 503 to trigger a retry
              resp = HttpResponse()
              resp.status_code = 503
          else:
              resp = JsonResponse(values)
      except Exception as e:
          print("Getting values from Yahoo! Finance failed:")
          print(e)
          resp = HttpResponse()
          resp.status_code = 500
      finally:
          getStocksRunning = False
          return resp
    else:
        resp = HttpResponse()
        resp.status_code = 503
        return resp
      
@login_required
@check_player_and_lost
def buyStocks(request):
    """Adds a Transaction entry times the amount.
    TODO: what happens on wrong entry (eg chars in amount or not existing symbol
    TODO: save all at the same time instead (atomic rollbacks)
    """
    if request.method == "POST":
        player = Player.objects.get(pk=request.user)
        stock = request.POST["stockSymbol"]
        amount = int(request.POST["amountStocks"])
        costPerStock = __getSingleQuote(stock)
        cost = round(costPerStock * amount, 2)
        if cost <= player.capital:
            player.capital -= cost
            player.save()
            for x in range(amount):
                newTrans = Transaction()
                newTrans.symbol = Stock.objects.get(pk=stock)
                newTrans.player = player
                newTrans.boughtfor = costPerStock
                newTrans.save()
            # return success http
            statusCode = 200
        else:
            statusCode = 400
        resp = HttpResponse()
        resp.status_code = statusCode
        return resp

@login_required
@check_player_and_lost
def sellStocks(request):
    """Adds a selldate to one or multiple Transactions.
    TODO: save all at the same time instead (atomic transactions)
    """
    if request.method == "POST":
        player = Player.objects.get(pk=request.user)
        stock = request.POST["stockSymbol"]
        amount = int(request.POST["amountStocks"])
        stocks = Transaction.objects.filter(player=player, symbol=stock, selldate=None)
        stockInv = __getSingleQuoteFromQuerySet(stocks)
        if stock in stockInv and stockInv[stock]["amount"] >= amount:
            gain = round(amount * stockInv[stock]["value"], 2)
            player.capital += gain
            player.save()
            # this will pick the oldest first
            for idx in range(len(stocks)):
                if idx >= amount:
                    break
                stocks[idx].selldate = timezone.now()
                stocks[idx].soldfor = stockInv[stock]["value"]
                stocks[idx].save()
            # return success http
            resp = HttpResponse()
            resp.status_code = 200
            return resp
        else:
            resp = HttpResponse()
            resp.status_code = 400
            return resp


### helper functions incl. speaking to backend

def __hasLost(player, values):
    """ returns None if not lost, otherwise the amount of lostcapitaltimesten"""
    minMoney = player.startcapital*(1-(player.acceptedloss/100))
    minMoney = round(minMoney, 2)
    capital = round(values["_all"] * player.capital, 2)
    if capital < minMoney:
        return int(capital*10)
    else:
        return None


def __getStockValuesFor(qs_stocks):
    """ gets values for stocks, and their entirety (key: "_all")
    returns in format of __createRqDict with "value" and "total_value" keys for the nested dicts, as well as _all

    return: {"symbol": {"amount": 3, "description": "Test Inc", "value": 3, "total_value": 9}, "_all": 9}
    """
    values = __createRqDict(qs_stocks)
    return StockPriceScraper.getStockQuotes(values)

def __getSingleQuote(stock: str):
    """returns the value  of a single quote
    """
    if Stock.objects.filter(pk=stock).exists():
        return StockPriceScraper.getQuote(stock)
    else:
        raise Exception("Not available")

def __getSingleQuoteFromQuerySet(qs_stock):
    """get quote for a single stock in the form of __getStockValuesFor
    empty querysets return an empty dict
    
    return: {"symbol": {"amount": 3, "description": "Test Inc", "value": 3, "total_value": 9}, "_all": 9}
    """
    if qs_stock:
      dict_stock = __createRqDict(qs_stock)
      str_stock = qs_stock[0].symbol.symbol
      dict_stock[str_stock]["value"] = __getSingleQuote(str_stock)
      return dict_stock
    else:
      return {}
    

def __createRqDict(qs_stocks):
    """creates a dict for stockselector

    return: {"symbol": {"amount": 3, "description": "Test Inc"}}
    """
    values = {}
    for stock in qs_stocks:
        stock = stock.symbol.symbol
        if stock not in values:
            values[stock] = {
                    "amount": 1,
                    "description": Stock.objects.get(pk=stock).description,
            }
        else:
            values[stock]["amount"] += 1
    return values


